JavaScript 是單執行緒的語言,所以一次只能執行一件事。事件循環 Event loop 是可以確保作為單行緒語言的 Javascript 可以在執行環境(瀏覽器或Node.js)中執行非同步 (asynchronous)程式碼而不會阻塞主執行緒的機制。
Javascript 的主執行緒開始執行 scripts,並且將同步任務放入執行棧 (call stack),直到該任務完成後才會被移除。
當執行時遇到非同步的任務時,例如:呼叫 api 或 setTimeout(),執行環境就會呼叫 Web API or Node.js API 讓其在背景運作。
等待非同步的任務獲得結果後,將其 callback 放到任務隊列(task queues)中。
隊列 queue: 是一種資料結構,特色是先進先出,可以想像是排隊的概念。
任務隊列(task queues)又可以分成微任務和宏任務:
微任務 Micro task queue:
宏任務 Macro task queue
當執行棧 (call stack)裡面所有的同步任務被執行完,就會去讀取任務隊列(task queues),然後把任務隊列的第一個任務加到執行棧 (call stack)執行。
微任務優先於宏任務
這個過程會一直無限循環下去,Javascript 有效地去處理非同步與同步的運行,並且可以避免主執行緒阻塞。
* 文字較為抽象,這個影片的說明更有助於理解整個觀念:
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
// This is a JavaScript Quiz from BFE.dev
console.log(1);
const promise = new Promise((resolve) => {
console.log(2);
resolve();
console.log(3);
});
console.log(4);
promise
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
console.log(7);
setTimeout(() => {
console.log(8);
}, 10);
setTimeout(() => {
console.log(9);
}, 0);
console.log(1),印出 1
const promise = new Promise((resolve) => {...}中的 console.log(2),印出2,console.log(3),印出3
console.log(4),印出4
promise.then(() => {console.log(5)}).then(() => {console.log(6)})的 callback 放到微任務隊列,console.log(7),印出7
setTimeout(() => {console.log(8)}, 10)放到宏任務隊列,等待 10ms 後執行。setTimeout(() => {console.log(9)}, 0)放到宏任務隊列,但因為延遲是 0ms,所以這個宏任務會在所有同步代碼執行完後的下一次事件循環中優先執行。promise.then(() => {console.log(5)}).then(() => {console.log(6)})放到 call stack 執行console.log(5),印出5
console.log(6)印出6
setTimeout(() => {console.log(9)}, 0),印出9
setTimeout(() => {console.log(8)}, 10),印出8
// This is a JavaScript Quiz from BFE.dev
console.log(1);
setTimeout(() => {
console.log(2);
}, 10);
setTimeout(() => {
console.log(3);
}, 0);
new Promise((_, reject) => {
console.log(4);
reject(5);
console.log(6);
})
.then(() => console.log(7))
.catch(() => console.log(8))
.then(() => console.log(9))
.catch(() => console.log(10))
.then(() => console.log(11))
.then(console.log)
.finally(() => console.log(12));
console.log(13);
執行console.log(1),印出1
把 setTimeout(() => { console.log(2); }, 10)放到宏任務隊列,延遲 10ms 後執行。
把 setTimeout(() => { console.log(3); }, 0)放到宏任務隊列中,延遲 0ms,會在事件循環的下一輪執行。
執行new Promise((_, reject) => {...})
console.log(4),印出4
reject(5),將 .catch() 的回調放入微任務隊列,但不會立即執行console.log(6),印出6
執行console.log(13),印出13
所有同步的程式碼都執行完了,檢查微任務隊列
reject(5)觸發.catch(() => console.log(8)),印出8,.then(() => console.log(9)),印出9
.then(() => console.log(11))印出11
.then(console.log),印出undefined
.finally(() => console.log(12)),印出12
檢查微任務隊列,發現已清空,執行宏任務隊列
setTimeout(() => {console.log(3);}, 0);印出3
setTimeout(() => {console.log(2);}, 10);印出2
const createPromise = () => Promise.resolve(1);
function func1() {
createPromise().then(console.log);
console.log(2);
}
async function func2() {
await createPromise();
console.log(3);
}
console.log(4);
func1();
func2();
執行 console.log(4),印出4
執行 func1()
createPromise().then(console.log),這裡 Promise.resolve(1) 會立即 resolve,將 console.log 放到微任務列隊console.log(2),印出2
執行 async function func2()
await createPromise(),此時createPromise()會立即 resolve,並暫停 func2 的執行,await 將控制權交還給主執行緒,並將剩下的部分console.log(3)放入微任務隊列所有同步程式碼執行完畢,檢查微任務列隊
console.log,又console.log為Promise.resolve(1)的callback,故會印出1
async function func2(),執行console.log(3),印出3